"""
Traceback standard module plus some additional APIs.
"""

import inspect
import logging
import pickle
from pprint import pformat
from traceback import format_exception


def tb_info(exc_info):
    """
    Prepare traceback info.

    :param exc_info: Exception info produced by sys.exc_info()
    """
    exc_type, exc_value, exc_traceback = exc_info
    return format_exception(exc_type, exc_value, exc_traceback.tb_next)


def prepare_exc_info(exc_info):
    """
    Prepare traceback info.

    :param exc_info: Exception info produced by sys.exc_info()
    """
    return "".join(tb_info(exc_info))


def log_exc_info(exc_info, logger=None):
    """
    Log exception info to logger_name.

    :param exc_info: Exception info produced by sys.exc_info()
    :param logger: Name or logger instance (defaults to '')
    """
    logger = logger or __name__
    if isinstance(logger, str):
        logger = logging.getLogger(logger)
    logger.error("")
    called_from = inspect.currentframe().f_back
    logger.error(
        "Reproduced traceback from: %s:%s",
        called_from.f_code.co_filename,
        called_from.f_lineno,
    )
    for trace in tb_info(exc_info):
        for line in trace.splitlines():
            logger.error(line)
    logger.error("")


def log_message(message, logger=None):
    """
    Log message to logger.

    :param message: Message
    :param logger: Name or logger instance (defaults to '')
    """
    logger = logger or __name__
    if isinstance(logger, str):
        logger = logging.getLogger(logger)
    for line in message.splitlines():
        logger.error(line)


def analyze_unpickable_item(path_prefix, obj):
    """
    Recursive method to obtain unpickable objects along with location

    :param path_prefix: Path to this object
    :param obj: The sub-object under introspection
    :return: [($path_to_the_object, $value), ...]
    """
    _path_prefix = path_prefix
    try:
        if hasattr(obj, "items"):
            subitems = obj.items()
            path_prefix += "[%s]"
        elif isinstance(obj, list):
            subitems = enumerate(obj)
            path_prefix += "[%s]"
        elif hasattr(obj, "__iter__"):
            subitems = enumerate(iter(obj))
            path_prefix += "<%s>"
        elif hasattr(obj, "__dict__"):
            subitems = obj.__dict__.items()
            path_prefix += ".%s"
        else:
            return [(path_prefix, obj)]
    except Exception:  # pylint: disable=W0703
        return [(path_prefix, obj)]
    unpickables = []
    for key, value in subitems:
        try:
            pickle.dumps(value)
        except pickle.PickleError:
            ret = analyze_unpickable_item(path_prefix % key, value)
            if ret:
                unpickables.extend(ret)
    if not unpickables:
        return [(_path_prefix, obj)]
    return unpickables


def str_unpickable_object(obj):
    """
    Return human readable string identifying the unpickable objects

    :param obj: The object for analysis
    :raise ValueError: In case the object is pickable
    """
    try:
        pickle.dumps(obj)
    except pickle.PickleError:
        pass
    else:
        raise ValueError(f"This object is pickable:\n{pformat(obj)}")
    unpickables = analyze_unpickable_item("this", obj)
    return "Unpickable object in:\n  %s\nItems causing troubles:\n  " % "\n  ".join(
        pformat(obj).splitlines()
    ) + "\n  ".join(
        "%s => %s" % obj for obj in unpickables  # pylint: disable=C0209
    )
